// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Thumbnail Mode.
 * @param {!HTMLElement} container A container.
 * @param {!ErrorBanner} errorBanner Error banner.
 * @param {!GalleryDataModel} dataModel Gallery data model.
 * @param {!cr.ui.ListSelectionModel} selectionModel List selection model.
 * @param {function()} changeToSlideModeCallback A callback to be called to
 *     change to slide mode.
 * @constructor
 * @extends {cr.EventTarget}
 * @struct
 */
function ThumbnailMode(container, errorBanner, dataModel, selectionModel,
    changeToSlideModeCallback) {
  /**
   * @private {!ErrorBanner}
   * @const
   */
  this.errorBanner_ = errorBanner;

  /**
   * @private {!GalleryDataModel}
   * @const
   */
  this.dataModel_ = dataModel;

  /**
   * @private {function()}
   * @const
   */
  this.changeToSlideModeCallback_ = changeToSlideModeCallback;

  this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));

  this.thumbnailView_ = new ThumbnailView(container, dataModel, selectionModel);
  this.thumbnailView_.addEventListener(
      'thumbnail-double-click', this.onThumbnailDoubleClick_.bind(this));
}

/**
 * Mode must extend cr.EventTarget.
 */
ThumbnailMode.prototype.__proto__ = cr.EventTarget.prototype;

/**
 * Returns name of this mode.
 * @return {string} Mode name.
 */
ThumbnailMode.prototype.getName = function() { return 'thumbnail'; };

/**
 * Returns title of this mode.
 * @return {string} Mode title.
 */
ThumbnailMode.prototype.getTitle = function() { return 'GALLERY_THUMBNAIL'; };

/**
 * Returns current sub mode.
 * @return {Gallery.SubMode}
 */
ThumbnailMode.prototype.getSubMode = function() {
  return Gallery.SubMode.BROWSE;
};

/**
 * Executes an action. An action is executed immediately since this mode does
 * not have busy state.
 */
ThumbnailMode.prototype.executeWhenReady = function(action) { action(); };

/**
 * @return {boolean} Always true. Toolbar is always visible.
 */
ThumbnailMode.prototype.hasActiveTool = function() { return true; };

/**
 * Handles key down event.
 * @param {!Event} event An event.
 * @return {boolean} True when an event is handled.
 */
ThumbnailMode.prototype.onKeyDown = function(event) {
  switch (event.keyIdentifier) {
    case 'Enter':
      if (event.target.matches('li.thumbnail')) {
        this.changeToSlideModeCallback_();
        return true;
      }
      break;
  }

  return false;
};

/**
 * Handles splice event of data model.
 */
ThumbnailMode.prototype.onSplice_ = function() {
  if (this.dataModel_.length === 0)
    this.errorBanner_.show('GALLERY_NO_IMAGES');
  else
    this.errorBanner_.clear();
};

/**
 * Handles thumbnail double click event of Thumbnail View.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailMode.prototype.onThumbnailDoubleClick_ = function(event) {
  this.changeToSlideModeCallback_();
};

/**
 * Shows thumbnail view.
 */
ThumbnailMode.prototype.show = function() {
  this.thumbnailView_.show();
};

/**
 * Hides thumbnail view.
 */
ThumbnailMode.prototype.hide = function() {
  this.thumbnailView_.hide();
};

/**
 * Performs thumbnail mode enter animation.
 * @param {number} index Selected thumbnail index.
 * @param {!ImageRect} rect A rect from which the transformation starts.
 */
ThumbnailMode.prototype.performEnterAnimation = function(index, rect) {
  this.thumbnailView_.performEnterAnimation(index, rect);
};

/**
 * Focus to thumbnail mode.
 */
ThumbnailMode.prototype.focus = function() {
  this.thumbnailView_.focus();
};

/**
 * Returns thumbnail rect of the index.
 * @param {number} index An index of thumbnail.
 * @return {!ClientRect} A rect of thumbnail.
 */
ThumbnailMode.prototype.getThumbnailRect = function(index) {
  return this.thumbnailView_.getThumbnailRect(index);
};

/**
 * Thumbnail view.
 * @param {!HTMLElement} container A container.
 * @param {!GalleryDataModel} dataModel Gallery data model.
 * @param {!cr.ui.ListSelectionModel} selectionModel List selection model.
 * @constructor
 * @extends {cr.EventTarget}
 * @struct
 *
 * TODO(yawano): Optimization. Remove DOMs outside of viewport, reuse them.
 * TODO(yawano): Extract ThumbnailView as a polymer element.
 */
function ThumbnailView(container, dataModel, selectionModel) {
  cr.EventTarget.call(this);

  /**
   * @private {!HTMLElement}
   */
  this.container_ = container;

  /**
   * @private {!GalleryDataModel}
   */
  this.dataModel_ = dataModel;

  /**
   * @private {!cr.ui.ListSelectionModel}
   */
  this.selectionModel_ = selectionModel;

  /**
   * @private {!Object}
   */
  this.thumbnails_ = {};

  /**
   * @private {boolean}
   */
  this.scrolling_ = false;

  /**
   * @private {number}
   */
  this.initialScreenY_ = 0;

  /**
   * @private {number}
   */
  this.initialScrollTop_ = 0;

  /**
   * @private {number}
   */
  this.scrollbarTimeoutId_ = 0;

  /**
   * @private {!HTMLElement}
   */
  this.list_ = assertInstanceof(document.createElement('ul'), HTMLElement);
  this.container_.appendChild(this.list_);

  /**
   * @private {!HTMLElement}
   */
  this.scrollbar_ = assertInstanceof(
      document.createElement('div'), HTMLElement);
  this.scrollbar_.classList.add('scrollbar');

  /**
   * @private {!HTMLElement}
   */
  this.scrollbarThumb_ = assertInstanceof(
      document.createElement('div'), HTMLElement);
  this.scrollbarThumb_.classList.add('thumb');
  this.scrollbar_.appendChild(this.scrollbarThumb_);
  this.container_.appendChild(this.scrollbar_);

  /**
   * @private {!HTMLElement}
   */
  this.animationThumbnail_ = assertInstanceof(
      document.createElement('div'), HTMLElement);
  this.animationThumbnail_.classList.add('animation-thumbnail');
  this.container_.appendChild(this.animationThumbnail_);

  this.container_.addEventListener('scroll', this.onScroll_.bind(this));
  this.container_.addEventListener('click', this.onClick_.bind(this));
  this.container_.addEventListener('dblclick', this.onDblClick_.bind(this));

  // Set tabIndex to -1 as the container can capture keydown events.
  this.container_.tabIndex = -1;
  this.container_.addEventListener('keydown', this.onKeydown_.bind(this));

  this.scrollbarThumb_.addEventListener(
      'mousedown', this.onScrollbarThumbMouseDown_.bind(this));
  window.addEventListener('mousemove', this.onWindowMouseMove_.bind(this));
  window.addEventListener('mouseup', this.onWindowMouseUp_.bind(this));

  this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
  this.dataModel_.addEventListener('content', this.onContent_.bind(this));
  this.selectionModel_.addEventListener(
      'change', this.onSelectionChange_.bind(this));
}

ThumbnailView.prototype.__proto__ = cr.EventTarget.prototype;

/**
 * Row height.
 * @const {number}
 *
 * TODO(yawano): Change so that Gallery adjust row height depending on image
 *     collection and window size to cover viewport as much as possible.
 */
ThumbnailView.ROW_HEIGHT = 160; // px

/**
 * Margins between thumbnails. This should be synced with CSS.
 * @const {number}
 */
ThumbnailView.MARGIN = 4; // px

/**
 * Timeout to fade out scrollbar.
 * @const {number}
 */
ThumbnailView.SCROLLBAR_TIMEOUT = 1500; // ms

/**
 * Selection mode.
 * @enum {string}
 */
ThumbnailView.SelectionMode = {
  SINGLE: 'single',
  MULTIPLE: 'multiple',
  RANGE: 'range'
};

/**
 * Shows thumbnail view.
 */
ThumbnailView.prototype.show = function() {
  this.container_.hidden = false;
};

/**
 * Hides thumbnail view.
 */
ThumbnailView.prototype.hide = function() {
  this.container_.hidden = true;
};

/**
 * Handles scroll bar thumb mouse down event.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onScrollbarThumbMouseDown_ = function(event) {
  this.scrolling_ = true;
  this.initialScreenY_ = event.screenY;
  this.initialScrollTop_ = this.container_.scrollTop;
};

/**
 * Handles mouse move event of window.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onWindowMouseMove_ = function(event) {
  if (this.scrolling_) {
    var diff = event.screenY - this.initialScreenY_;
    var scrollTop = this.initialScrollTop_ +
        (diff * this.container_.scrollHeight / this.scrollbar_.clientHeight);
    this.container_.scrollTop = scrollTop;
  }

  this.resetTimerOfScrollbar_();
};

/**
 * Handles mouse up event of window.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onWindowMouseUp_ = function(event) {
  this.scrolling_ = false;
  this.resetTimerOfScrollbar_();
};

/**
 * Handles scroll of viewport.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onScroll_ = function(event) {
  this.updateScrollBar_();
};

/**
 * Moves selection to specified direction.
 * @param {string} direction Direction. Should be 'Left', 'Right', 'Up', or
 *     'Down'.
 * @param {boolean} selectRange True to perform range selection.
 * @private
 */
ThumbnailView.prototype.moveSelection_ = function(direction, selectRange) {
  var step;
  if ((direction === 'Left' && !isRTL()) ||
      (direction === 'Right' && isRTL()) ||
      (direction === 'Up')) {
    step = -1;
  } else if ((direction === 'Right' && !isRTL()) ||
             (direction === 'Left' && isRTL()) ||
             (direction === 'Down')) {
    step = 1;
  } else {
    assertNotReached();
  }

  var vertical = direction === 'Up' || direction === 'Down';
  var baseIndex = this.selectionModel_.leadIndex !== -1 ?
      this.selectionModel_.leadIndex :
      this.selectionModel_.selectedIndex;
  var baseRect = this.getThumbnailRect(baseIndex);
  var baseCenter = baseRect.left + baseRect.width / 2;
  var minHorizontalGap = Number.MAX_VALUE;
  var index = null;

  for (var i = baseIndex + step;
       0 <= i && i < this.dataModel_.length;
       i += step) {
    // Skip error thumbnail.
    var thumbnail = this.getThumbnailAt_(i);
    if (thumbnail.isError())
      continue;

    // Look for the horizontally nearest item if it is vertical move. Otherwise
    // it just use the current i.
    if (vertical) {
      var rect = this.getThumbnailRect(i);
      var verticalGap = Math.abs(baseRect.top - rect.top);
      if (verticalGap === 0)
        continue;
      else if (verticalGap >= ThumbnailView.ROW_HEIGHT * 2)
        break;
      // If centerGap - rect.width / 2 < 0, the image is located just
      // above the center point of base image since baseCenter is in the range
      // (rect.left, rect.right). In this case we use 0 as distance. Otherwise
      // centerGap - rect.width / 2 equals to the distance between baseCenter
      // and either of rect.left or rect.right that is closer to centerGap.
      var centerGap = Math.abs(baseCenter - (rect.left + rect.width / 2));
      var horizontalGap = Math.max(centerGap - rect.width / 2, 0);
      if (horizontalGap < minHorizontalGap) {
        minHorizontalGap = horizontalGap;
        index = i;
      }
    } else {
      index = i;
      break;
    }
  }

  if (index !== null) {
    // Move selection.
    if (selectRange && this.selectionModel_.anchorIndex !== -1) {
      // Since anchorIndex will be set to 0 by unselectAll, copy the value.
      var anchorIndex = this.selectionModel_.anchorIndex;
      this.selectionModel_.unselectAll();
      this.selectionModel_.selectRange(anchorIndex, index);
      this.selectionModel_.anchorIndex = anchorIndex;
    } else {
      this.selectionModel_.selectedIndex = index;
      this.selectionModel_.anchorIndex = index;
    }

    this.selectionModel_.leadIndex = index;
    this.scrollTo_(index);
  }
};

/**
 * Scrolls viewport to show the thumbnail of the index.
 * @param {number} index Index of a thumbnail which becomes to appear in the
 *     viewport.
 * @private
 *
 * TODO(yawano): Add scroll animation.
 */
ThumbnailView.prototype.scrollTo_ = function(index) {
  var thumbnailRect = this.getThumbnailRect(index);

  if (thumbnailRect.top - ThumbnailView.MARGIN < ImageEditor.Toolbar.HEIGHT) {
    this.container_.scrollTop -=
        ImageEditor.Toolbar.HEIGHT - thumbnailRect.top + ThumbnailView.MARGIN;
  } else if (thumbnailRect.bottom + ThumbnailView.MARGIN >
      this.container_.clientHeight) {
    this.container_.scrollTop += thumbnailRect.bottom + ThumbnailView.MARGIN -
        this.container_.clientHeight;
  }
};

/**
 * Updates scroll bar.
 * @private
 */
ThumbnailView.prototype.updateScrollBar_ = function() {
  var scrollTop = this.container_.scrollTop;
  var scrollHeight = this.container_.scrollHeight;
  var clientHeight = this.container_.clientHeight;

  // If viewport is not long enough to scroll, do not show scrollbar.
  if (scrollHeight <= clientHeight) {
    this.scrollbar_.hidden = true;
    return;
  }

  this.scrollbar_.hidden = false;

  var thumbHeight =
      ~~(this.scrollbar_.clientHeight * clientHeight / scrollHeight);
  var thumbTop = ~~(scrollTop * this.scrollbar_.clientHeight / scrollHeight);

  this.scrollbarThumb_.style.height = thumbHeight + 'px';
  this.scrollbarThumb_.style.marginTop = thumbTop + 'px';

  this.resetTimerOfScrollbar_();
};

/**
 * Resets timer to fade out scrollbar. If scrollbar is already faded-out, this
 * method makes it visible and set timeout. If user is scrolling, this method
 * just clears existing timer.
 * @private
 */
ThumbnailView.prototype.resetTimerOfScrollbar_ = function() {
  this.scrollbar_.classList.toggle('transparent', false);

  if (this.scrollbarTimeoutId_) {
    clearTimeout(this.scrollbarTimeoutId_);
    this.scrollbarTimeoutId_ = 0;
  }

  // If user is scrolling, do not set timeout.
  if (this.scrolling_)
    return;

  this.scrollbarTimeoutId_ = setTimeout(function() {
    this.scrollbarTimeoutId_ = 0;
    this.scrollbar_.classList.toggle('transparent', true);
  }.bind(this), ThumbnailView.SCROLLBAR_TIMEOUT);
};

/**
 * Handles splice event of data model.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onSplice_ = function(event) {
  if (event.removed) {
    for (var i = 0; i < event.removed.length; i++) {
      this.remove_(event.removed[i]);
    }
  }

  if (event.added && event.added.length > 0) {
    // Get a thumbnail before which new thumbnail is inserted.
    var insertBefore = null;
    var galleryItem = this.dataModel_.item(event.index + event.added.length);
    if (galleryItem)
      insertBefore = this.thumbnails_[galleryItem.getEntry().toURL()];

    for (var i = 0; i < event.added.length; i++) {
      this.insert_(event.added[i], insertBefore);
    }
  }
};

/**
 * Handles content event of data model.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onContent_ = function(event) {
  var galleryItem = event.item;
  var oldEntry = event.oldEntry;
  var thumbnail = this.thumbnails_[oldEntry.toURL()];
  if (thumbnail) {
    // Update map.
    delete this.thumbnails_[oldEntry.toURL()];
    this.thumbnails_[galleryItem.getEntry().toURL()] = thumbnail;

    thumbnail.update();
  }
};

/**
 * Handles selection change event.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onSelectionChange_ = function(event) {
  var changes = event.changes;
  var lastSelectedThumbnail = null;

  for (var i = 0; i < changes.length; i++) {
    var change = changes[i];

    var galleryItem = this.dataModel_.item(change.index);
    if (!galleryItem)
      continue;

    var thumbnail = this.thumbnails_[galleryItem.getEntry().toURL()];
    if (!thumbnail)
      continue;

    thumbnail.setSelected(change.selected);

    // We should not focus to error thumbnail.
    if (change.selected && !thumbnail.isError())
      lastSelectedThumbnail = thumbnail;
  }

  // If new item is selected, focus to it. If multiple thumbnails are selected,
  // focus to the last one.
  if (lastSelectedThumbnail)
    lastSelectedThumbnail.getContainer().focus();
};

/**
 * Handles click event.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onClick_ = function(event) {
  var target = event.target;
  if (target.matches('.selection.frame')) {
    var selectionMode = ThumbnailView.SelectionMode.SINGLE;
    if (event.ctrlKey)
      selectionMode = ThumbnailView.SelectionMode.MULTIPLE;
    if (event.shiftKey)
      selectionMode = ThumbnailView.SelectionMode.RANGE;

    this.selectByThumbnail_(target.parentNode.getThumbnail(), selectionMode);
    return;
  }

  // If empty space is clicked, unselect current selection.
  this.selectionModel_.unselectAll();
};

/**
 * Handles double click event.
 * @param {!Event} event An event.
 * @private
 */
ThumbnailView.prototype.onDblClick_ = function(event) {
  var target = event.target;
  if (target.matches('.selection.frame')) {
    this.selectByThumbnail_(target.parentNode.getThumbnail(),
        ThumbnailView.SelectionMode.SINGLE);
    var thumbnailDoubleClickEvent = new Event('thumbnail-double-click');
    this.dispatchEvent(thumbnailDoubleClickEvent);
  }
};

/**
 * Handles keydown event.
 * @param {!Event} event
 * @private
 */
ThumbnailView.prototype.onKeydown_ = function(event) {
  var keyString = util.getKeyModifiers(event) + event.keyIdentifier;

  switch (keyString) {
    case 'Right':
    case 'Left':
    case 'Up':
    case 'Down':
    case 'Shift-Right':
    case 'Shift-Left':
    case 'Shift-Up':
    case 'Shift-Down':
      this.moveSelection_(event.keyIdentifier, event.shiftKey);
      event.stopPropagation();
      break;
    case 'Ctrl-U+0041': // Crtl+A
      this.selectionModel_.selectAll();
      event.stopPropagation();
      break;
  }
};

/**
 * Selects a thumbnail.
 * @param {!ThumbnailView.Thumbnail} thumbnail Thumbnail to be selected.
 * @param {ThumbnailView.SelectionMode} selectionMode
 * @private
 */
ThumbnailView.prototype.selectByThumbnail_ = function(
    thumbnail, selectionMode) {
  var index = this.dataModel_.indexOf(thumbnail.getGalleryItem());

  if (selectionMode === ThumbnailView.SelectionMode.SINGLE) {
    this.selectionModel_.unselectAll();
    this.selectionModel_.setIndexSelected(index, true);
    this.selectionModel_.anchorIndex = index;
  } else if (selectionMode === ThumbnailView.SelectionMode.MULTIPLE) {
    this.selectionModel_.setIndexSelected(index,
        this.selectionModel_.selectedIndexes.indexOf(index) === -1);
  } else if (selectionMode === ThumbnailView.SelectionMode.RANGE) {
    var leadIndex = this.selectionModel_.leadIndex;
    this.selectionModel_.unselectAll();
    this.selectionModel_.selectRange(leadIndex, index);
  } else {
    assertNotReached();
  }

  this.selectionModel_.leadIndex = index;
};

/**
 * Inserts an item.
 * @param {!GalleryItem} galleryItem A gallery item.
 * @param {!ThumbnailView.Thumbnail} insertBefore A thumbnail before which new
 *     thumbnail is inserted. Set null for adding at the end of the list.
 * @private
 */
ThumbnailView.prototype.insert_ = function(galleryItem, insertBefore) {
  var thumbnail = new ThumbnailView.Thumbnail(galleryItem);
  this.thumbnails_[galleryItem.getEntry().toURL()] = thumbnail;
  if (insertBefore) {
    this.list_.insertBefore(
        thumbnail.getContainer(), insertBefore.getContainer());
  } else {
    this.list_.appendChild(thumbnail.getContainer());
  }

  // Set selection state.
  var index = this.dataModel_.indexOf(galleryItem);
  thumbnail.setSelected(this.selectionModel_.getIndexSelected(index));

  this.updateScrollBar_();
};

/**
 * Removes an item.
 * @param {!GalleryItem} galleryItem A gallery item.
 * @private
 */
ThumbnailView.prototype.remove_ = function(galleryItem) {
  var thumbnail = this.thumbnails_[galleryItem.getEntry().toURL()];
  this.list_.removeChild(thumbnail.getContainer());
  delete this.thumbnails_[galleryItem.getEntry().toURL()];
};

/**
 * Returns thumbnail instance at specified index.
 * @param {number} index Index of the thumbnail.
 * @return {!ThumbnailView.Thumbnail} Thumbnail at the index.
 * @private
 */
ThumbnailView.prototype.getThumbnailAt_ = function(index) {
  var galleryItem = this.dataModel_.item(index);
  return this.thumbnails_[galleryItem.getEntry().toURL()];
};

/**
 * Returns a rect of the specified thumbnail.
 * @param {number} index An index of the thumbnail.
 * @return {!ClientRect} Rect of the thumbnail.
 */
ThumbnailView.prototype.getThumbnailRect = function(index) {
  var thumbnail = this.getThumbnailAt_(index);
  return thumbnail.getContainer().getBoundingClientRect();
};

/**
 * Performs enter animation.
 * @param {number} index Index of the thumbnail which is animated.
 * @param {!ImageRect} rect A rect from which the transformation starts.
 *
 * TODO(yawano): Consider to move this logic to thumbnail mode.
 */
ThumbnailView.prototype.performEnterAnimation = function(index, rect) {
  this.scrollTo_(index);
  this.updateScrollBar_();

  var thumbnailRect = this.getThumbnailRect(index);
  var thumbnail = this.getThumbnailAt_(index);

  // If thumbnail is not loaded yet or failed to load, do not perform animation.
  if (!thumbnail.getBackgroundImage() || thumbnail.isError())
    return;

  // Hide animating thumbnail.
  thumbnail.setTransparent(true);

  this.animationThumbnail_.style.backgroundImage =
      thumbnail.getBackgroundImage();
  this.animationThumbnail_.classList.add('animating');
  this.animationThumbnail_.width = thumbnail.getWidth();
  this.animationThumbnail_.height = ThumbnailView.ROW_HEIGHT;

  var animationPlayer = this.animationThumbnail_.animate([{
    height: rect.height + 'px',
    left: rect.left + 'px',
    top: rect.top + 'px',
    width: rect.width + 'px',
    offset: 0,
    easing: 'linear'
  }, {
    height: thumbnailRect.height + 'px',
    left: thumbnailRect.left + 'px',
    top: thumbnailRect.top + 'px',
    width: thumbnailRect.width + 'px',
    offset: 1
  }], 250);

  animationPlayer.addEventListener('finish', function() {
    this.animationThumbnail_.classList.remove('animating');
    thumbnail.setTransparent(false);
  }.bind(this));
};

/**
 * Focus to thumbnail view. If an item is selected, focus to it.
 */
ThumbnailView.prototype.focus = function() {
  if (this.selectionModel_.selectedIndexes.length === 0) {
    this.container_.focus();
    return;
  }

  var index = this.selectionModel_.leadIndex !== -1 ?
      this.selectionModel_.leadIndex : this.selectionModel_.selectedIndex;
  var thumbnail = this.getThumbnailAt_(index);
  thumbnail.getContainer().focus();
};

/**
 * Thumbnail.
 * @param {!GalleryItem} galleryItem A gallery item.
 * @constructor
 * @struct
 */
ThumbnailView.Thumbnail = function(galleryItem) {
  /**
   * @private {!GalleryItem}
   */
  this.galleryItem_ = galleryItem;

  /**
   * @private {boolean}
   */
  this.selected_ = false;

  /**
   * @private {ThumbnailLoader}
   */
  this.thumbnailLoader_ = null;

  /**
   * @private {number}
   */
  this.thumbnailLoadRequestId_ = 0;

  /**
   * @private {number}
   */
  this.width_ = 0;

  /**
   * @private {*}
   */
  this.error_ = null;

  /**
   * @private {!HTMLElement}
   */
  this.container_ = assertInstanceof(document.createElement('li'), HTMLElement);
  this.container_.tabIndex = 1;
  this.container_.classList.add('thumbnail');

  /**
   * @private {!HTMLElement}
   */
  this.imageFrame_ = assertInstanceof(
      document.createElement('div'), HTMLElement);
  this.imageFrame_.classList.add('image', 'frame');
  this.container_.appendChild(this.imageFrame_);

  /**
   * @private {!HTMLElement}
   */
  this.selectionFrame_ = assertInstanceof(
      document.createElement('div'), HTMLElement);
  this.selectionFrame_.classList.add('selection', 'frame');
  this.container_.appendChild(this.selectionFrame_);

  this.container_.style.height = ThumbnailView.ROW_HEIGHT + 'px';
  this.container_.getThumbnail =
      function(thumbnail) { return thumbnail; }.bind(null, this);

  this.update();
};

/**
 * Returns a gallery item.
 * @return {!GalleryItem} A gallery item.
 */
ThumbnailView.Thumbnail.prototype.getGalleryItem = function() {
  return this.galleryItem_;
};

/**
 * Change selection state of this thumbnail.
 * @param {boolean} selected True to make this thumbnail selected.
 */
ThumbnailView.Thumbnail.prototype.setSelected = function(selected) {
  this.selected_ = selected;
  this.container_.classList.toggle('selected', selected);
};

/**
 * Returns a container.
 * @return {!HTMLElement} A container.
 */
ThumbnailView.Thumbnail.prototype.getContainer = function() {
  return this.container_;
};

/**
 * Sets this thumbnail as transparent.
 * @param {boolean} transparent True to make this thumbnail transparent.
 */
ThumbnailView.Thumbnail.prototype.setTransparent = function(transparent) {
  this.container_.classList.toggle('transparent', transparent);
};

/**
 * Returns width of this thumbnail.
 * @return {number} Width of this thumbnail.
 */
ThumbnailView.Thumbnail.prototype.getWidth = function() {
  return this.width_;
};

/**
 * Returns whether this has failed to load thumbnail or not.
 * @return {boolean} True if thumbnail load has failed.
 */
ThumbnailView.Thumbnail.prototype.isError = function() {
  return !!this.error_;
};

/**
 * Sets error.
 * @param {*} error Error object. Set null to clear error.
 * @private
 */
ThumbnailView.Thumbnail.prototype.setError_ = function(error) {
  this.error_ = error;
  this.container_.classList.toggle('error', !!this.error_);
};

/**
 * Sets width of this thumbnail.
 * @param {number} width Width.
 * @private
 */
ThumbnailView.Thumbnail.prototype.setWidth_ = function(width) {
  if (this.width_ === width)
    return;

  this.width_ = width;
  this.container_.style.width = this.width_ + 'px';
};

/**
 * Returns background image style of this thumbnail.
 * @return {string} Background image.
 */
ThumbnailView.Thumbnail.prototype.getBackgroundImage = function() {
  return this.imageFrame_.style.backgroundImage;
};

/**
 * Updates thumbnail.
 */
ThumbnailView.Thumbnail.prototype.update = function() {
  // Update title.
  this.container_.setAttribute('title', this.galleryItem_.getFileName());

  // Calculate and set width.
  var metadata = this.galleryItem_.getMetadataItem();
  if (!metadata) {
    this.setWidth_(ThumbnailView.ROW_HEIGHT);
    return;
  }

  var rotated = metadata.imageRotation % 2 === 1;
  var imageWidth = rotated ? metadata.imageHeight : metadata.imageWidth;
  var imageHeight = rotated ? metadata.imageWidth : metadata.imageHeight;
  this.setWidth_(~~(imageWidth * ThumbnailView.ROW_HEIGHT / imageHeight));

  // Set thumbnail.
  var thumbnailMetadata = this.galleryItem_.getThumbnailMetadataItem();
  if (!thumbnailMetadata)
    return;

  this.loadAndSetThumbnail_(thumbnailMetadata,
      false /* do not force to generate thumbnail */).then(function(result) {
    if (!result ||
        result.height >= ThumbnailView.ROW_HEIGHT ||
        result.loadTarget === ThumbnailLoader.LoadTarget.FILE_ENTRY ||
        metadata.imageHeight <= ThumbnailView.ROW_HEIGHT ||
        (thumbnailMetadata.external && !thumbnailMetadata.external.present)) {
      return;
    }

    // If thumbnail height is lower than ThumbnailView.ROW_HEIGHT, generate
    // thumbnail from image content.
    this.loadAndSetThumbnail_(
        thumbnailMetadata, true /* force to generate thumbnail */);
  }.bind(this));
};

/**
 * Loads thumbnail and sets it.
 * @param {!ThumbnailMetadataItem} thumbnailMetadata
 * @param {boolean} forceToGenerate True to force generating thumbnail from
 *     image content.
 * @return {!Promise<?{height:number, loadTarget:?ThumbnailLoader.LoadTarget}>}
 *     null is returned for error case.
 * @private
 */
ThumbnailView.Thumbnail.prototype.loadAndSetThumbnail_ = function(
    thumbnailMetadata, forceToGenerate) {
  this.thumbnailLoadRequestId_++;

  var loadTargets = forceToGenerate ?
      [ThumbnailLoader.LoadTarget.FILE_ENTRY] :
      undefined /* default value */;

  this.thumbnailLoader_ = new ThumbnailLoader(this.galleryItem_.getEntry(),
      undefined /* opt_loaderType */, thumbnailMetadata,
      undefined /* opt_mediaType */, loadTargets);
  return this.thumbnailLoader_.loadAsDataUrl(
      ThumbnailLoader.FillMode.FIT).then(function(requestId, result) {
    // Discard the result of old request.
    if (requestId !== this.thumbnailLoadRequestId_)
      return null;

    // Update width by using the width of actual data.
    this.setWidth_(
        ~~(result.width * ThumbnailView.ROW_HEIGHT / result.height));

    this.imageFrame_.style.backgroundImage = 'url(' + result.data + ')';
    this.setError_(null);

    return {
      height: result.height,
      loadTarget: this.thumbnailLoader_.getLoadTarget()
    };
    }.bind(this, this.thumbnailLoadRequestId_))
    .catch(function(requestId, error) {
      if (requestId !== this.thumbnailLoadRequestId_)
        return null;

      this.setError_(error);
      return null;
    }.bind(this, this.thumbnailLoadRequestId_));
};
